/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.meducated.sim.model.def;

import com.meducated.sim.model.Effector;
import com.meducated.sim.model.Model;
import com.meducated.sim.model.Periodic;
import com.meducated.sim.model.Property;
import com.meducated.sim.model.PropertyDependencyGraph;
import com.meducated.sim.model.Sensor;
import com.meducated.sim.model.Timing;
import java.util.HashSet;
import java.util.Iterator;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 *
 * @author Carina
 */
public class DefaultModel extends Model {

    
    /* ---------------------------------------------------------------------- */
    /**
     *
     * @param <T>
     */
    private abstract class Scheduled<T> implements Comparable<Scheduled> {
        //

        protected double t;
        //
        protected final T object;

        //
        private Scheduled(double t, T object) {
            this.t = t;
            this.object = object;
        }

        //
        public abstract void run();

        //
        @Override
        public boolean equals(Object o) {
            try {
                Scheduled<T> s = (Scheduled<T>) o;
                if (this.t == s.t) {
                    return object.equals(o);
                } else {
                    return false;
                }
            } catch (ClassCastException cce) {
                return false;
            }

        }

        /**
         *
         * @param t
         * @return
         */
        @Override
        public int compareTo(Scheduled t) {
            if (this.t == t.t) {
                return this.object.hashCode() - t.object.hashCode();
            } else {
                return (int) (this.t - t.t);
            }
        }
    }
    
    private class ScheduledSensor extends Scheduled <Sensor> {
        //
        protected Property prop;
        
        protected ScheduledSensor(Sensor s, Property prop, double t) {
            super(t, s);
            this.prop = prop;
        }

        @Override
        public void run() {
            runSensorInternal(this.object, this.prop, this.t);
        }
    }
    
      private class ScheduledEffector extends Scheduled <Effector> {
        //
        protected Property prop;
        
        protected ScheduledEffector(Effector e, Property prop, double t) {
            super(t, e);
            this.prop = prop;
        }

        @Override
        public void run() {
            runEffectorInternal(this.object, this.prop, this.t);
        }
    }
      
       private class ScheduledPeriodic extends Scheduled <Periodic> {
        
        protected ScheduledPeriodic(Periodic p, double t) {
            super(t, p);
        }

        @Override
        public void run() {
            runInternal(this.object, this.t);
        }
    }
    
    /* ---------------------------------------------------------------------- */
    /**
     *
     */
    private double t = 0;
    /**
     *
     */
    private double dt = 1;
    /**
     *
     */
    private final PriorityQueue<Scheduled> eventQueue = new PriorityQueue<Scheduled>();
    
    /**
     *
     */
    private final Set<Property> properties = new HashSet<Property>();
    /**
     *
     */
    private final Set<Sensor> sensors = new HashSet<Sensor>();
    /**
     *
     */
    private final Set<Effector> effectors = new HashSet<Effector>();
    
   
    /**
     * 
     */
    private final PropertyDependencyGraph dependencyGraph = new PropertyDependencyGraph();

    /**
     * 
     */
    private final Set <Property> effectedLog = new TreeSet<Property>(dependencyGraph);
   
    /**
     *
     * @param p
     */
    private void tickInternal() {
        this.t = this.t + this.dt; 

        /* System.out.println("t=" + t + " s"); */
        
        long startTime = System.nanoTime();

      
        while (!eventQueue.isEmpty() && eventQueue.peek().t <= this.t) {
            
            double tt = eventQueue.peek().t;
            effectedLog.clear();
            
            while(eventQueue.peek().t == tt) {
                Scheduled s = eventQueue.remove();
                s.run();
            }
            
            for(Property prop : effectedLog) {
                //System.out.println("Updating " + prop.getName());
                prop.getConnector().integrate(tt, dt);
            }
            effectedLog.clear();
        }
        
        long estimatedTime = System.nanoTime() - startTime;
        
        //this.t = this.t - ((double)estimatedTime * (double)(10 ^ -12));

        if (eventQueue.isEmpty()) {
            //
        } else {
            this.dt = eventQueue.peek().t - this.getT();
        }
  }

    /**
     *
     * @param s
     * @param t
     */
    private void runSensorInternal(Sensor s, Property prop, double t) {
        s.sense(prop, t);
        this.schedule(s, prop, t + s.getSensingRate());
    }

    /**
     *
     * @param s
     * @param t
     */
    private void runEffectorInternal(Effector e, Property prop, double t) {
        prop.getConnector().putFactor(e, e.effect(t));
        
        effectedLog.add(prop);
        
        SortedSet <Property> dependents;
        if ((dependents = dependencyGraph.getAllDependents(prop)) !=null) {
            effectedLog.addAll(dependents);
        }
        this.schedule(e, prop, t + e.getEffectingRate());
        
    }
    
    /**
     * 
     * @param p
     * @param t 
     */
    private void runInternal(Periodic p, double t) {
        p.run(t);
        this.schedule(p, t + p.getInterval());
    }

    /**
     *
     * @param s
     * @param t
     */
    private void schedule(final Sensor s, final Property prop, final double t) {
        this.eventQueue.add(new ScheduledSensor(s, prop, t));
        
    }

    /**
     *
     * @param s
     * @param t
     */
    private void schedule(final Effector e, final Property prop, final double t) {
       this.eventQueue.add(new ScheduledEffector(e, prop, t));
    }
    
    /**
     * 
     * @param p
     * @param t 
     */
    private void schedule(final Periodic p, final double t) {
        this.eventQueue.add(new ScheduledPeriodic(p, t));
    }

    /**
     *
     */
    public void run() {
        tickInternal();
    }

    /**
     *
     */
    public void runStandaloneFor(final double duration) {
        final ExecutorService executor = Executors.newFixedThreadPool(1);

        final Runnable timerTask = new Runnable() {
            @Override
            public void run() {

                while (t < duration) {
                    DefaultModel.this.run();
                    try {
                        Thread.sleep((long) dt * 1000);
                    } catch (InterruptedException ex) {
                    }
                }
                executor.shutdownNow();
            }
        };

        executor.execute(timerTask);
    }

    /**
     *
     * @param p
     */
    public void add(Property p) {
        properties.add(p);
        
        for (Iterator it = p.getConnector().getDependencies().iterator(); it.hasNext();) {
            Property d = (Property)it.next();
            dependencyGraph.add(d, p);
        }
        if(p.getConnector().getIntegrator() != null) {
            p.getConnector().integrate(getT(), this.getDt());
        }
    }

    /**
     *
     * @param s
     */
    public void add(Sensor s, Property p) {
        sensors.add(s);
        schedule(s, p, getT() + Timing.MIN_INTERVAL/* s.getSensingRate() */);
    }

    /**
     *
     * @param s
     */
    public void add(Effector e, Property p) {
        effectors.add(e);
        schedule(e, p, getT() + Timing.MIN_INTERVAL /* e.getEffectingRate()*/);
    }
    
    /**
     * 
     */
    public void add(Periodic p) {
        this.schedule(p, getT());
    }
    
    /**
     * @return the t
     */
    public double getT() {
        return t;
    }

    /**
     * @return the dt
     */
    public double getDt() {
        return dt;
    }

    
    
}
